package com.nekonomics
package domain.service

import api.dto.response.*
import domain.auxiliary.SimpleOrderItem
import domain.detail.CartItemDetail
import domain.entity.Order
import error.{OrderError, ProductError, UserError}
import persistence.query.{CartItemDetailRepo, OrderItemDetailRepo, SkuDetailRepo, SkuSpecDetailRepo}
import persistence.repository.*
import types.{Money, OrderStatus}
import utils.DoobieUtil.*

import cats.syntax.traverse.*
import com.nekonomics.config.AliPayConfigProvider
import doobie.*
import doobie.free.connection.ConnectionIO
import doobie.hikari.HikariTransactor
import doobie.implicits.*
import zio.*

import java.time.OffsetDateTime


trait OrderService {
  def confirmDetail(userId: Long, skuIds: Set[Long]): DomainIO[OrderConfirmDto]

  def submitOrder(userId: Long, skuIds: Set[Long], addressId: Long, note: Option[String]): DomainIO[Order]

  def list(userId: Long): DomainIO[List[OrderDto]]

  def getOrder(orderId: Long): DomainIO[Option[Order]]

  def checkIfOrderExpired(orderId: Long): DomainIO[Boolean]

  def orderDelivered(): DomainIO[Unit]
  
  def receivedOrder(orderId: Long): DomainIO[Unit]

}

object OrderService {
  val live: TaskLayer[OrderService] = (config.layers ++ persistence.layers) >>> ZLayer.fromFunction(OrderServiceLive.apply)
}


case class OrderServiceLive(
  xa: HikariTransactor[Task],
  orderRepo: OrderRepo,
  orderItemRepo: OrderItemRepo,
  skuRepo: SkuRepo,
  addressRepo: AddressRepo,
  cartItemDetailRepo: CartItemDetailRepo,
  merchantRepo: MerchantRepo,
  skuSpecDetailRepo: SkuSpecDetailRepo,
  cartItemRepo: CartItemRepo,
  orderItemDetailRepo: OrderItemDetailRepo,
  config: AliPayConfigProvider,

) extends OrderService {


  override def confirmDetail(userId: Long, skuIds: Set[Long]): DomainIO[OrderConfirmDto] = {
    val tx = for {
      ciDetails <- cartItemDetailRepo.listBy(userId, skuIds.toList)
      _ <- raise(ProductError.SkuNotFound) when ciDetails.length < skuIds.size
      totalPrice = ciDetails.foldLeft(Money(0))((acc, ci) => acc + ci.price * ci.quantity)
      addresses <- addressRepo.findByUserId(userId)
      ciDetails <- ciDetails.map(ci => skuSpecDetailRepo.listSpecsBySkuId(ci.skuId).map(ci -> _)).sequence
      tupleList <- ciDetails.map(tp => merchantRepo.getBySpuId(tp._1.spuId).map(_ -> tp)).sequence
      grouped = tupleList.groupMap(_._1)(_._2).toList
      cisByMct = grouped.map { case (mct, cis) => CartItemsByMerchant.of(mct, cis) }
    } yield OrderConfirmDto(
      groups = cisByMct, addresses = addresses.map(AddressDto.of), totalPrice = totalPrice
    )
    tx.convertWithXa(xa)
  }


  override def submitOrder(userId: Long, skuIds: Set[Long], addressId: Long, note: Option[String]): DomainIO[Order] = {
    val tx = for {
      cartItems <- checkAndLockStock(userId, skuIds.toList)
      (orderId, _) <- createOrder(userId, cartItems, addressId, note)
    } yield orderId
    for {
      orderId <- tx.convertWithXa(xa)
      order <- orderRepo.findById(orderId).convertWithXa(xa).someOrFail(OrderError.OrderNotFound)
    } yield order

  }


  /**
   * 涉及到sku表，减库存
   */
  private def checkAndLockStock(userId: Long, skuIds: List[Long]): ConnectionIO[List[CartItemDetail]] = {
    for {
      // cart_item表该保证该用户购物车中sku id不会重复；包含了与sku表的join，
      cartItemDetails <- cartItemDetailRepo.listBy(userId, skuIds)
      // 判断sku不存在、已下架
      thatSkuIds = cartItemDetails.map(_.skuId)
      _ <- raise(ProductError.SkuNotFound) when skuIds.exists(!thatSkuIds.contains(_))
      // 锁定库存
      affected <- skuRepo.reserveStockCID(cartItemDetails)
      _ <- raise(ProductError.StockInsufficient) when affected < cartItemDetails.length
    } yield cartItemDetails
  }

  /**
   * 涉及到order、order_item、payment表
   */
  private def createOrder(userId: Long, items: List[CartItemDetail], addressId: Long, note: Option[String]): ConnectionIO[(Long, Int)] = {
    val totalPrice: Money = items.foldLeft(Money(0))((acc, ci) => acc + ci.price * ci.quantity)
    for {
      address <- addressRepo.findById(addressId)
      _ <- raise(UserError.AddressNotFound) when address.isEmpty
      orderId <- orderRepo.insert(userId, OrderStatus.Pending, totalPrice, note, address.get)
      orderItems = items.map(ci => SimpleOrderItem(orderId, ci.skuId, ci.quantity, ci.price))
      affected <- orderItemRepo.batchInsert(orderItems)
    } yield (orderId, affected)
  }


  override def list(userId: Long): DomainIO[List[OrderDto]] = {
    val tx = for {
      items <- orderItemDetailRepo.listByUserId(userId)
      orderDtoList = items.groupBy(_.orderId).map { (orderId, orderItems) =>
        val head = orderItems.head
        OrderDto(
          id = orderId,
          createdAt = head.createdAt,
          status = head.orderStatus,
          total = orderItems.foldLeft(Money(0)) { (acc, oi) => acc + oi.price * oi.quantity },
          items = orderItems.map(OrderItemDetailDto.of)
        )
      }.toList.sortBy(_.createdAt)(Ordering[OffsetDateTime].reverse) // 按创建时间倒序
    } yield orderDtoList
    tx.convertWithXa(xa)
  }


  override def getOrder(orderId: Long): DomainIO[Option[Order]] = {
    orderRepo.findById(orderId).convertWithXa(xa)
  }


  override def checkIfOrderExpired(orderId: Long): DomainIO[Boolean] = {
    orderRepo.findById(orderId)
      .convertWithXa(xa)
      .someOrFail(OrderError.OrderNotFound)
      .map(order =>
        OffsetDateTime.now.isAfter(order.createdAt.plusMinutes(config.orderDurationMinutes))
      )
  }

  override def orderDelivered(): DomainIO[Unit] = {
    orderRepo
      .listByShipped(durationMinute = config.orderDurationMinutes)
      .map(_.map(_.id))
      .convertWithXa(xa)
      .flatMap { ids =>
        ZIO.foreachDiscard(ids)(orderRepo.updateStatusById(_, OrderStatus.Delivered).convertWithXa(xa))
      }
  }


  override def receivedOrder(orderId: Long): DomainIO[Unit] = {
    orderRepo
      .updateStatusById(orderId, OrderStatus.Completed)
      .convertWithXa(xa)
      .unit
  }
}
